#include <math.h>
#include <QtCore/QCryptographicHash>
#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QTimerEvent>
#include <QyDebug.h>
#include "QpwNetwork.h"

using namespace QpwNetwork;

/**************************************************************************************************/

inline void initialize() {

    static bool initialized = false;

    if (initialized)
        return;

    initialized = true;

    QyNet::Kernel::registerFactory(
        new QyNetUtils::DatagramEncoderFactory<VariantDatagram>(VariantDatagramType) );
    QyNet::Kernel::registerFactory(
        new QyNetUtils::DatagramEncoderFactory<FilePartDatagram>(FilePartDatagramType) );

}

inline QString completeSlash( const QString & path ) {

    if ( path.isEmpty() )
        return path;

    return ( path[ path.count() - 1 ] == '/' ) ? path : path + "/";
}

inline QByteArray calcFileHash( const QString & filePath ) {

    QByteArray hash;
    QFile file(filePath);

    if ( file.open(QIODevice::ReadOnly) ) {

        //TODO: если файл большой, считать хэш по кускам (use QCryptographicHash::addData)
        hash = QCryptographicHash::hash( file.readAll(), QCryptographicHash::Md5 );
        file.close();
    }

    return hash;
}

/**************************************************************************************************/

namespace QpwNetwork {
enum DataTypes {

    // messages from client
    LoginPassDataType = VariantDatagram::SystemVariantDataType,

    // message from server
    LoginQueryDataType,
    UuidDataType,

    // sync messages
    GetAllFileInfoDataType,
    FileInfoDataType,
    GetFileDataType,

};
};

const quint32 unauthorisedTimeLife = 5000;

/**************************************************************************************************/

VariantDatagram::VariantDatagram( int dataType, const QVariant & data1, const QVariant & data2,
    const QVariant & data3, const QVariant & data4, const QVariant & data5 )
{

    VariantDatagram::dataType = dataType;

//     qDebug( "VariantDatagram(%d)", dataType );
//     qDebug( "  data1: %s", qPrintable( data1.toString() ) );
//     qDebug( "  data2: %s", qPrintable( data2.toString() ) );
//     qDebug( "  data3: %s", qPrintable( data3.toString() ) );
//     qDebug( "  data4: %s", qPrintable( data4.toString() ) );
//     qDebug( "  data5: %s", qPrintable( data5.toString() ) );

    if ( data1.isValid() ) {

        data.append(data1);

        if ( data2.isValid() ) {

            data.append(data2);

            if ( data3.isValid() ) {

                data.append(data3);

                if ( data4.isValid() ) {

                    data.append(data4);

                    if ( data5.isValid() )
                        data.append(data5);

                }

            }

        }

    }

//     qDebug( "  data count: %d", data.count() );

}

QByteArray VariantDatagram::encode() const {

    QByteArray dataArray;
    QDataStream stream( &dataArray, QIODevice::WriteOnly );
    stream.setVersion(QDataStream::Qt_4_0);
    stream << dataType;
    stream << data;

    return dataArray;
}

void VariantDatagram::decode( QByteArray dataArray ) {

    QDataStream stream( &dataArray, QIODevice::ReadOnly );
    stream.setVersion(QDataStream::Qt_4_0);
    stream >> dataType;
    stream >> data;

}

/**************************************************************************************************/

QByteArray FilePartDatagram::encode() const {

    QByteArray dataArray;
    QDataStream stream( &dataArray, QIODevice::WriteOnly );
    stream.setVersion(QDataStream::Qt_4_0);
    stream << totalSize;
    stream << pos;
    stream << totalParts;
    stream << part;
    stream << fileName;
    stream << filePath;
    stream << fileData;

//         QyDbgLocation();
//         QyDbgValue(dataArray.size());
//         QyDbgValue(totalSize);
//         QyDbgValue(pos);
//         QyDbgValue(part);
//         QyDbgValue(fileData.size());
//         QyDbgValue(fileName);

    return dataArray;
}

void FilePartDatagram::decode( QByteArray dataArray ) {

    QDataStream stream( &dataArray, QIODevice::ReadOnly );
    stream.setVersion(QDataStream::Qt_4_0);
    stream >> totalSize;
    stream >> pos;
    stream >> totalParts;
    stream >> part;
    stream >> fileName;
    stream >> filePath;
    stream >> fileData;

//         QyDbgLocation();
//         QyDbgValue(dataArray.size());
//         QyDbgValue(totalSize);
//         QyDbgValue(pos);
//         QyDbgValue(part);
//         QyDbgValue(data.size());
//         QyDbgValue(fileName);

}

bool FilePartDatagram::write( const QString & filePath ) {

    if ( filePath.isEmpty() )
        return false;

    QFile file(filePath);

//         if ( file.exists() && file.size() != totalSize )
//             file.resize(totalSize);

    if ( file.open(QIODevice::Append) ) {

//             file.resize(totalSize);
        file.seek(pos);
        if ( file.pos() != pos )
            qWarning("QpwNetwork::FilePartDatagram: Can't setup true file pos.");
        if ( file.write(fileData) != fileData.size() )
            qWarning("QpwNetwork::FilePartDatagram: Can't write data to file.");
        file.close();

        return true;

    } else {

        qWarning( "QpwNetwork::FilePartDatagram: Can't write to file %s", qPrintable(filePath) );

    }

    return false;
}

/*static */bool FilePartDatagram::sendFile( const QString & filePath,
    QyNet::ConnectionRecord * clientRecord, quint32 blockSize )
{

    if ( filePath.isEmpty() )
        return false;

    QFile file(filePath);

    if ( file.open(QIODevice::ReadOnly) ) {

        quint64 totalSize = file.size();
        quint32 totalPartCount = quint32( ceil( totalSize / qreal(blockSize) ) );

//             QyDbgValue(parts);
//             QyDbgValue(lastFullPartIndex);
//             QyDbgValue(totalPartCount);
        FilePartDatagram filePartData;
        filePartData.totalSize = totalSize;
        filePartData.totalParts = totalPartCount;
        filePartData.fileName = QFileInfo(filePath).fileName();
        filePartData.filePath = filePath;

        for ( filePartData.part = 0; filePartData.part < totalPartCount;//lastFullPartIndex;
            ++filePartData.part )
        {

            filePartData.pos = file.pos();
            filePartData.fileData = file.read(blockSize);
//                 qDebug( "Send file data size: %d", int(filePartData.fileData.size()) );
            clientRecord->send(&filePartData);

//                 break;
        }

        file.close();

        return true;
    } else {

        qWarning( "QpwNetwork::FilePartDatagram: File \"%s\" sending error: %s", qPrintable(filePath),
            qPrintable( file.errorString() ) );

    }

    return false;
}

/**************************************************************************************************/

ClientRecord::ClientRecord( QTcpSocket * socket, Server * server )
    : QyNet::ConnectionRecord( socket, server ), p_server(server)
{

    p_killTimerId = startTimer(unauthorisedTimeLife);

    connect( this, SIGNAL( datagramReceived(QyNet::AbstractDatagramEncoder*) ),
        SLOT( onDatagramReceived(QyNet::AbstractDatagramEncoder*) ) );
    connect( this, SIGNAL( disconnected() ),
        SLOT( onDisconnected() ) );

}

void ClientRecord::logout() {

    if ( !p_uuid.isNull() ) {

        p_server->accountManager()->logoutUser(p_login);
        p_uuid = QUuid();

    }

}

void ClientRecord::onDatagramReceived( QyNet::AbstractDatagramEncoder * datagram ) {

//     qDebug( "ClientRecord: Received datagram %d[%s]", datagram->type(),
//         datagram->type() == VariantDatagramType ? "variant" : "unknow" );

    if ( datagram->type() != VariantDatagramType ) {

        if ( !isAuthorised() ) {

            qWarning( "QpwNetwork::ClientRecord: Received message from unauthorised client %s",
                qPrintable( socket()->peerAddress().toString() ) );
            socket()->disconnectFromHost();

        }

        return;
    }

    if ( VariantDatagram * variant = dynamic_cast<VariantDatagram*>(datagram) ) {

        switch (variant->dataType) {

        case LoginPassDataType:

//             qDebug( "ClientRecord: data count %d", variant->data.count() );

            if ( variant->data.count() == 2 ) {

                p_login = variant->data[0].toString();

                if ( p_server->accountManager()->loginUser( p_login, variant->data[1].toString() ) ) {

                    killTimer(p_killTimerId);
                    p_uuid = QUuid::createUuid();
//                     qDebug( "ClientRecord: send uuid %s", qPrintable( p_uuid.toString() ) );
                    VariantDatagram data( UuidDataType, p_uuid.toString() );
                    send(&data);
                    emit authorised();

                } else {

                    qWarning( "QpwNetwork::ClientRecord: bad login message from unauthorised client %s",
                        qPrintable( socket()->peerAddress().toString() ) );
                    socket()->disconnectFromHost();

                }

            }

            break;

        }

    }

}

void ClientRecord::onDisconnected() {

    // nothing

}

void ClientRecord::timerEvent( QTimerEvent * event ) {

    if ( event->timerId() != p_killTimerId )
        return QObject::timerEvent(event);

    killTimer(p_killTimerId);
    socket()->disconnectFromHost();

}

/**************************************************************************************************/

Server::Server( QObject * parent ) : QyNet::Server(parent) {

    initialize();
    p_synchronizer = new ContentSynchronizer( QString(), this );
    p_accountManager = new AccountManager;

}

Server::Server( quint16 port, const QHostAddress & address, QObject * parent )
    : QyNet::Server( port, address, parent )
{

    initialize();
    p_synchronizer = new ContentSynchronizer( QString(), this );
    p_accountManager = new AccountManager;

}

Server::~Server() {
}

void Server::sendAllAuthorised( QyNet::AbstractDatagramEncoder * datagram ) {

    foreach ( ClientRecord * client, p_authorised )
        client->send(datagram);

}

void Server::clientAuthorised() {

    ClientRecord * clientRecord = qobject_cast<ClientRecord*>( sender() );

    connect( clientRecord, SIGNAL( disconnected() ), SLOT( clientDisconnected() ) );

    p_authorised.append(clientRecord);
    p_synchronizer->syncTo(clientRecord);

    emit clientAuthorised(clientRecord);

}

void Server::clientDisconnected() {

    ClientRecord * clientRecord = qobject_cast<ClientRecord*>( sender() );
    p_authorised.removeAll(clientRecord);
    emit clientDisconnected(clientRecord);
    clientRecord->logout();

}

QyNet::ConnectionRecord * Server::createConnectionRecord( QTcpSocket * socket ) {

//     qDebug( "Server: new connection %s", qPrintable( socket->peerAddress().toString() ) );

    ClientRecord * clientRecord = new ClientRecord( socket, this );
    VariantDatagram query(LoginQueryDataType);

    connect( clientRecord, SIGNAL( authorised() ), SLOT( clientAuthorised() ) );

//     qDebug("Server: send login query");
    clientRecord->send(&query);

    return clientRecord;
}

/**************************************************************************************************/

Client::Client( const QHostAddress & address, quint16 port, QObject * parent )
    : QyNet::Client( address, port, parent )
{

    init();

}

void Client::login( const QHostAddress & address, quint16 port, const QString & username,
    const QString & password )
{

    p_uuid = QUuid();
    p_login = username;
    p_password = password;
//     qDebug( "Client: Connecting to %s:%d", qPrintable( address.toString() ), port );
    connectToHost( address, port );
    // QCryptographicHash::hash( password, QCryptographicHash::Md5 )

}

void Client::onDatagramReceived( QyNet::AbstractDatagramEncoder * datagram ) {

//     qDebug( "Client: Received datagram %d[%s]", datagram->type(),
//         datagram->type() == VariantDatagramType ? "variant" : "unknow" );

    if ( datagram->type() != VariantDatagramType ) {

        if ( !isAuthorised() ) {

            qWarning("QpwNetwork::Client: Received data message before authorising.");
            socket()->disconnectFromHost();

        }

        return;
    }

    if ( VariantDatagram * variant = dynamic_cast<VariantDatagram*>(datagram) ) {

//         qDebug( "Client: data count %d", variant->data.count() );

        switch (variant->dataType) {

        case LoginQueryDataType:
            {
//                 qDebug( "Client: Send login %s, %s", qPrintable(p_login), qPrintable(p_password) );
                VariantDatagram answer( LoginPassDataType, p_login, p_password );
                send(&answer);
            }
            break;

        case UuidDataType:

            if ( variant->data.count() == 1 ) {

                p_uuid = QUuid( variant->data[0].toString() );
                emit authorised();

            }

            break;

        }

    }

}

void Client::onDisconnected() {

    if ( !p_uuid.isNull() )
        p_uuid = QUuid();

}

void Client::init() {

    ::initialize();
    connect( this, SIGNAL( datagramReceived(QyNet::AbstractDatagramEncoder*) ),
        SLOT( onDatagramReceived(QyNet::AbstractDatagramEncoder*) ) );
    connect( socket(), SIGNAL( disconnected() ),
        SLOT( onDisconnected() ) );
    p_synchronizer = new ContentSynchronizer( QString(), this );
    p_synchronizer->connect( this, SIGNAL( datagramReceived(QyNet::AbstractDatagramEncoder*) ),
        SLOT( processIncommingSyncData(QyNet::AbstractDatagramEncoder*) ) );

}

/**************************************************************************************************/

ContentSynchronizer::FileInfo::FileInfo( const QString & filePath, const QString & basePath,
    bool calcHash ) : p_filePath(filePath)
{

    if (calcHash)
        p_hash = calcFileHash( completeSlash(basePath) + filePath );

    p_size = QFile( completeSlash(basePath) + filePath ).size();

}

/**************************************************************************************************/

ContentSynchronizer::ContentSynchronizer( const QString & basePath, QObject * parent )
    : QObject(parent),
    p_basePath(basePath),
    p_acceptFiles(true)
{
}

ContentSynchronizer::~ContentSynchronizer() {
}

bool ContentSynchronizer::checkFile( const QString & filePath, const QByteArray & hash ) {

    if ( filePath.isEmpty() || hash.isEmpty() )
        return false;

    return calcFileHash( completeSlash(p_basePath) + filePath ) == hash;
}

void ContentSynchronizer::addFile( const QString & filePath ) {

    FileInfo info( QDir(p_basePath).relativeFilePath(filePath), p_basePath );

//     qDebug( "QpwNetwork::Synchronizer: add file %s", qPrintable( info.filePath() ) );

    if ( !p_files.contains(info) )
        p_files.append(info);

}

void ContentSynchronizer::removeFile( const QString & filePath ) {

    FileInfo info( filePath, "", false );
    p_files.removeAll(info);

}

void ContentSynchronizer::syncFrom( QyNet::ConnectionRecord * connectionRecord ) {

    if (connectionRecord) {

        connect( connectionRecord, SIGNAL( datagramReceived(QyNet::AbstractDatagramEncoder*) ),
            SLOT( processIncommingSyncData(QyNet::AbstractDatagramEncoder*) ) );

        VariantDatagram getAllFileDatagram(GetAllFileInfoDataType);
        connectionRecord->send(&getAllFileDatagram);

    }

}

void ContentSynchronizer::syncTo( QyNet::ConnectionRecord * connectionRecord ) {

    if (connectionRecord) {

        connect( connectionRecord, SIGNAL( datagramReceived(QyNet::AbstractDatagramEncoder*) ),
            SLOT( processIncommingSyncData(QyNet::AbstractDatagramEncoder*) ) );

        sendAllFiles(connectionRecord);

    }

}

void ContentSynchronizer::addAllFrom( QString path, bool recursive ) {

//     qDebug( "QpwNetwork::Synchronizer: add all from %s", qPrintable(path) );

    if ( path.isEmpty() )
        path = basePath();
    else if ( p_basePath.isEmpty() )
        p_basePath = path;

    QDir dir(path);

    if ( path.isEmpty() || !dir.exists() ) {

        qWarning( "QpwNetwork::Synchronizer: can't add all from \"%s\"", qPrintable(path) );

        return;
    }

    foreach ( QFileInfo fileInfo,
        dir.entryInfoList( recursive ? QDir::AllEntries|QDir::NoDotAndDotDot : QDir::Files ) )
    {

        if ( fileInfo.isDir() )
            addAllFrom( fileInfo.filePath(), recursive );
        else
            addFile( fileInfo.filePath() );

    }

}

void ContentSynchronizer::processIncommingSyncData( QyNet::AbstractDatagramEncoder * datagram ) {

    QyNet::ConnectionRecord * receiver = qobject_cast<QyNet::ConnectionRecord*>( sender() );

    if (!receiver) {

        qWarning("QpwNetwork::Syncroniser: Received data from not valid object.");
        return;
    }

    switch ( datagram->type() ) {

    case VariantDatagramType:

        if ( VariantDatagram * variant = dynamic_cast<VariantDatagram*>(datagram) ) {

            switch (variant->dataType) {

            case GetAllFileInfoDataType:

//                 qDebug( "GetAllFileInfoDataType %d", variant->data.count() );
                if ( variant->data.count() == 0 )
                    sendAllFiles(receiver);

                break;

            case FileInfoDataType:

//                 qDebug( "FileInfoDataType %d", variant->data.count() );
                if ( variant->data.count() == 3 ) {

                    QString filePath = variant->data[0].toString();

//                     qDebug( "Checking file %s", qPrintable(filePath) );

                    if ( !checkFile(
                        filePath,
                        variant->data[1].toByteArray() ) )
                    {

//                         qDebug("hash check is fail, get file from master host");
                        p_fileLoadingInfos[filePath] = FileLoadingInfo( variant->data[2].toLongLong() );
                        VariantDatagram getFileDatagram( GetFileDataType, filePath );
                        receiver->send(&getFileDatagram);

                    }

                }

                break;

            case GetFileDataType:

//                 qDebug( "GetFileDataType %d", variant->data.count() );
                if ( variant->data.count() == 1 ) {

                    QString filePath = variant->data[0].toString();

//                     qDebug( "Requested %s file, sending ...", qPrintable(filePath) );

                    QString oldPath = QDir::currentPath();
                    QDir::setCurrent(p_basePath);
                    FilePartDatagram::sendFile( filePath, receiver );
                    QDir::setCurrent(oldPath);

                }

                break;

            }

        }

        break;

    case FilePartDatagramType:

        if (!p_acceptFiles)
            break;

//         qDebug( "FilePartDatagramType" );
        if ( FilePartDatagram * filePart = dynamic_cast<FilePartDatagram*>(datagram) ) {

            QString filePath = filePart->originalFilePath();

            if ( p_fileLoadingInfos.contains(filePath) ) {

                FileLoadingInfo & fileLoadingInfo = p_fileLoadingInfos[filePath];

                fileLoadingInfo.current += filePart->dataSize();

                if ( !QDir( completeSlash(p_basePath) ).mkpath( QFileInfo(filePath).dir().path() ) )
                    qWarning("QpwNetwork::Syncroniser: can't make path \"%s\"", qPrintable( QFileInfo(filePath).filePath() ) );

                filePart->write( completeSlash(p_basePath) + filePath );

//                 qDebug( "file loading: %s %d/%d", qPrintable(filePath), int( fileLoadingInfo.current ),
//                     int( fileLoadingInfo.size() ) );
                emit synchronisationProgress( filePath, fileLoadingInfo.current,
                    fileLoadingInfo.size() );

                if ( fileLoadingInfo.current == fileLoadingInfo.size() ) {

                    p_fileLoadingInfos.remove(filePath);

                    if ( p_fileLoadingInfos.isEmpty() )
                        emit synchronisationComplete();

                }

            } else {

//                 qDebug( "Received unknow file %s", qPrintable( filePart->originalFilePath() ) );

            }

        }

        break;

    }

}

void ContentSynchronizer::sendAllFiles( QyNet::ConnectionRecord * connectionRecord ) {

    if (!connectionRecord)
        return;

    VariantDatagram fileInfoDatagram(FileInfoDataType);

    fileInfoDatagram.data.append( QString() );
    fileInfoDatagram.data.append( QByteArray() );
    fileInfoDatagram.data.append( qint64(0) );

    foreach ( FileInfo info, p_files ) {

        fileInfoDatagram.data[0] = info.filePath();
        fileInfoDatagram.data[1] = info.hash();
        fileInfoDatagram.data[2] = info.size();
        connectionRecord->send(&fileInfoDatagram);

    }

}

/**************************************************************************************************/
